package keeper

import (
	"context"
	"fmt"
	"time"

	"cosmossdk.io/collections"
	"cosmossdk.io/core/address"
	storetypes "cosmossdk.io/core/store"
	"cosmossdk.io/log"
	types2 "github.com/cosmos/cosmos-sdk/contrib/x/crisis/types"

	"github.com/cosmos/cosmos-sdk/codec"
	sdk "github.com/cosmos/cosmos-sdk/types"
)

// Keeper - crisis keeper
//
// Deprecated: the crisis keeper is deprecated and will be removed in the next Cosmos SDK major release.
type Keeper struct {
	routes         []types2.InvarRoute
	invCheckPeriod uint
	storeService   storetypes.KVStoreService
	cdc            codec.BinaryCodec

	// the address capable of executing a MsgUpdateParams message. Typically, this
	// should be the x/gov module account.
	authority string

	supplyKeeper types2.SupplyKeeper

	feeCollectorName string // name of the FeeCollector ModuleAccount

	addressCodec address.Codec

	Schema      collections.Schema
	ConstantFee collections.Item[sdk.Coin]
}

// NewKeeper creates a new Keeper object
//
// Deprecated: the crisis keeper is deprecated and will be removed in the next Cosmos SDK major release.
func NewKeeper(
	cdc codec.BinaryCodec, storeService storetypes.KVStoreService, invCheckPeriod uint,
	supplyKeeper types2.SupplyKeeper, feeCollectorName, authority string, ac address.Codec,
) *Keeper {
	sb := collections.NewSchemaBuilder(storeService)
	k := &Keeper{
		storeService:     storeService,
		cdc:              cdc,
		routes:           make([]types2.InvarRoute, 0),
		invCheckPeriod:   invCheckPeriod,
		supplyKeeper:     supplyKeeper,
		feeCollectorName: feeCollectorName,
		authority:        authority,
		addressCodec:     ac,

		ConstantFee: collections.NewItem(sb, types2.ConstantFeeKey, "constant_fee", codec.CollValue[sdk.Coin](cdc)),
	}
	schema, err := sb.Build()
	if err != nil {
		panic(err)
	}
	k.Schema = schema
	return k
}

// GetAuthority returns the x/crisis module's authority.
func (k *Keeper) GetAuthority() string {
	return k.authority
}

// Logger returns a module-specific logger.
func (k *Keeper) Logger(ctx context.Context) log.Logger {
	sdkCtx := sdk.UnwrapSDKContext(ctx)
	return sdkCtx.Logger().With("module", "x/"+types2.ModuleName)
}

// RegisterRoute register the routes for each of the invariants
func (k *Keeper) RegisterRoute(moduleName, route string, invar sdk.Invariant) {
	invarRoute := types2.NewInvarRoute(moduleName, route, invar)
	k.routes = append(k.routes, invarRoute)
}

// Routes - return the keeper's invariant routes
func (k *Keeper) Routes() []types2.InvarRoute {
	return k.routes
}

// Invariants returns a copy of all registered Crisis keeper invariants.
func (k *Keeper) Invariants() []sdk.Invariant {
	invars := make([]sdk.Invariant, len(k.routes))
	for i, route := range k.routes {
		invars[i] = route.Invar
	}
	return invars
}

// AssertInvariants asserts all registered invariants. If any invariant fails,
// the method panics.
func (k *Keeper) AssertInvariants(ctx sdk.Context) {
	logger := k.Logger(ctx)

	start := time.Now()
	invarRoutes := k.Routes()
	n := len(invarRoutes)
	for i, ir := range invarRoutes {
		logger.Info("asserting crisis invariants", "inv", fmt.Sprint(i+1, "/", n), "name", ir.FullRoute())

		invCtx, _ := ctx.CacheContext()
		if res, stop := ir.Invar(invCtx); stop {
			// TODO: Include app name as part of context to allow for this to be
			// variable.
			panic(fmt.Errorf("invariant broken: %s\n"+
				"\tCRITICAL please submit the following transaction:\n"+
				"\t\t tx crisis invariant-broken %s %s", res, ir.ModuleName, ir.Route))
		}
	}

	diff := time.Since(start)
	logger.Info("asserted all invariants", "duration", diff, "height", ctx.BlockHeight())
}

// InvCheckPeriod returns the invariant checks period.
func (k *Keeper) InvCheckPeriod() uint { return k.invCheckPeriod }

// SendCoinsFromAccountToFeeCollector transfers amt to the fee collector account.
func (k *Keeper) SendCoinsFromAccountToFeeCollector(ctx context.Context, senderAddr sdk.AccAddress, amt sdk.Coins) error {
	return k.supplyKeeper.SendCoinsFromAccountToModule(ctx, senderAddr, k.feeCollectorName, amt)
}
